package com.stylismo.intellij.inspection;

import com.google.common.collect.Lists;
import com.intellij.codeInsight.NullableNotNullManager;
import com.intellij.codeInspection.BaseJavaLocalInspectionTool;
import com.intellij.codeInspection.InspectionManager;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiEnumConstant;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiPrimitiveType;
import com.intellij.psi.PsiType;
import com.intellij.psi.util.MethodSignatureBackedByPsiMethod;
import com.intellij.psi.util.TypeConversionUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import static com.intellij.codeInspection.ProblemHighlightType.GENERIC_ERROR_OR_WARNING;
import static com.intellij.psi.PsiModifier.FINAL;
import static com.intellij.psi.PsiModifier.PRIVATE;
import static com.intellij.psi.PsiModifier.STATIC;
import static com.stylismo.intellij.inspection.QuickFixFactory.createQuickFixes;


@Slf4j
public class NullabilityAnnotationsInspection extends BaseJavaLocalInspectionTool implements CommonInspection {
    private static final String MISSING_NULLABLE_NONNULL_ANNOTATION = "Missing @Nullable/@Nonnull annotation";
    @Setter
    @Getter
    private boolean reportFields = true;
    @Setter
    @Getter
    private boolean reportInitializedStaticFinalFields = true;
    @Setter
    @Getter
    private boolean reportInitializedFinalFields = true;
    @Setter
    @Getter
    private boolean reportPrivateMethods = true;
    @Setter
    @Getter
    private boolean removeRedundantAnnotations = true;

    @Nullable
    @Override
    public JComponent createOptionsPanel() {
        return new OptionsPanel(this);
    }

    @Nullable
    @Override
    public ProblemDescriptor[] checkField(PsiField field, InspectionManager manager, boolean isOnTheFly) {
        if (isFieldMissingNullAnnotation(field, field.getType())) {
            Collection<ProblemDescriptor> problemDescriptors = Lists.newArrayList();
            createProblemDescriptorWithQuickFixes(field, manager, problemDescriptors, field);
            return problemDescriptors.isEmpty()
                    ? null
                    : problemDescriptors.toArray(new ProblemDescriptor[problemDescriptors.size()]);
        }
        return null;
    }

    @Nullable
    @Override
    public ProblemDescriptor[] checkMethod(PsiMethod method, InspectionManager manager, boolean isOnTheFly) {
        if (isNonPrivateMethod(method)) {
            List<ProblemDescriptor> problemDescriptors = new ArrayList<>();
            List<MethodSignatureBackedByPsiMethod> superMethodSignatures = superMethods(method);
            PsiParameter[] parameters = method.getParameterList().getParameters();
            //方法的参数 param check groovy 更细化不需要了
            checkMethodParams(manager, problemDescriptors, superMethodSignatures, parameters);
            checkMethodReturnType(method, manager, problemDescriptors);
            return problemDescriptors.isEmpty()
                    ? null
                    : problemDescriptors.toArray(new ProblemDescriptor[problemDescriptors.size()]);
        }
        return null;
    }


    private void checkMethodReturnType(PsiMethod method,
                                       InspectionManager manager,
                                       List<ProblemDescriptor> aProblemDescriptors) {
        if (isMethodNeedAnnotation(method)) {
            // must be not null because of com.stylismo.intellij.inspection.NullabilityAnnotationsInspection#isMethodNeedAnnotation
            createProblemDescriptorWithQuickFixes(method, manager, aProblemDescriptors, method.getReturnTypeElement());
        }
    }

    public boolean isMethodNeedAnnotation(PsiMethod method) {
        //listOwner.getModifierList().findAnnotation(com.intellij.codeInsight.AnnotationUtil.NOT_NULL)
        final boolean hasAnnotation = hasAnnotation(method);
        log.debug("{}:{}", method.getName(), hasAnnotation);
        return !(method.isConstructor() || method.getReturnType() instanceof PsiPrimitiveType ||
                hasAnnotation || method.getReturnTypeElement() == null);
    }

    private void checkMethodParams(InspectionManager manager,
                                   List<ProblemDescriptor> aProblemDescriptors,
                                   List<MethodSignatureBackedByPsiMethod> aSuperMethodSignatures,
                                   PsiParameter[] aParameters) {
        for (int i = 0, parametersLength = aParameters.length; i < parametersLength; i++) {
            // param 底层 api
            PsiParameter parameter = aParameters[i];
            if (parameterNeedsAnnotation(parameter) && !hasAnnotation(parameter) && !hasAnnotationInHierarchy(i, aSuperMethodSignatures)) {
                createProblemDescriptorWithQuickFixes(parameter, manager, aProblemDescriptors, parameter);
            }
        }
    }

    private void createProblemDescriptorWithQuickFixes(PsiModifierListOwner owner,
                                                       InspectionManager manager,
                                                       Collection<ProblemDescriptor> problemDescriptors,
                                                       PsiElement element) {
        if (element.isPhysical()) {
            LocalQuickFix[] localQuickFixes = createQuickFixes(owner, isRemoveRedundantAnnotations());
            ProblemDescriptor problemDescriptor = manager.createProblemDescriptor(
                    element,
                    MISSING_NULLABLE_NONNULL_ANNOTATION,
                    localQuickFixes,
                    GENERIC_ERROR_OR_WARNING,
                    true,
                    false);
            problemDescriptors.add(problemDescriptor);
        }
    }

    public boolean isFieldMissingNullAnnotation(PsiField field, PsiType type) {
        return reportFields
                && shouldCheckField(field)
                && field.isPhysical()
                && !(field instanceof PsiEnumConstant)
                && !TypeConversionUtil.isPrimitiveAndNotNull(type)
                && !hasAnnotation(field);
    }

    private boolean shouldCheckField(PsiField field) {
        return !field.hasModifierProperty(FINAL) ||
                (field.hasModifierProperty(STATIC) ? reportInitializedStaticFinalFields : reportInitializedFinalFields)
                || !hasExpressionElement(field.getChildren());
    }

    @Contract(pure = true)
    private boolean hasExpressionElement(PsiElement[] psiElements) {
        for (PsiElement psiElement : psiElements) {
            if ((psiElement instanceof PsiExpression)) {
                return true;
            }
        }
        return false;
    }

    public boolean hasAnnotation(PsiModifierListOwner psiModifierListOwner) {
        // better
        return NullableNotNullManager.getInstance(psiModifierListOwner.getProject()).hasNullability(psiModifierListOwner);
/*        return NullableNotNullManager.isNullable(psiModifierListOwner)
                || NullableNotNullManager.isNotNull(psiModifierListOwner);*/
    }

    private boolean hasAnnotationInHierarchy(int parameter,
                                             List<MethodSignatureBackedByPsiMethod> superMethodSignatures) {
        for (MethodSignatureBackedByPsiMethod methodSignature : superMethodSignatures) {
            PsiMethod superMethod = methodSignature.getMethod();
            PsiParameter[] superParameters = superMethod.getParameterList().getParameters();
            PsiParameter superParameter = superParameters[parameter];
            if (hasAnnotation(superParameter)) {
                return true;
            }
        }
        return false;
    }

    @Done
    public boolean parameterNeedsAnnotation(PsiParameter parameter) {
        return !(parameter.getType() instanceof PsiPrimitiveType || parameter.isVarArgs());
    }

    private List<MethodSignatureBackedByPsiMethod> superMethods(PsiMethod method) {
        List<MethodSignatureBackedByPsiMethod> signatures = method.findSuperMethodSignaturesIncludingStatic(true);
        signatures.removeIf(superSignature ->
                superSignature.getMethod().getParameterList().getParametersCount()
                        != method.getParameterList().getParametersCount());
        return signatures;
    }

    private boolean isNonPrivateMethod(PsiMethod method) {
        return reportPrivateMethods || !method.hasModifierProperty(PRIVATE);
    }
}